package office.document.pdfbox;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.spire.doc.Document;
import com.spire.pdf.FileFormat;
import com.spire.pdf.PdfConvertOptions;
import com.spire.pdf.PdfDocument;
import com.spire.pdf.utilities.PdfTable;
import com.spire.pdf.utilities.PdfTableExtractor;
import lombok.extern.slf4j.Slf4j;
import office.CustomConstant;
import office.document.core.domain.DocumentInfoDO;
import office.document.core.domain.TableExtractDO;
import office.document.core.domain.TextLineExtractDO;
import office.document.docx4j.utils.WordMergeUtil;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import wiki.xsx.core.pdf.doc.XEasyPdfDocument;
import wiki.xsx.core.pdf.handler.XEasyPdfHandler;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

/**
 * pdf提取工具类
 * - 支持提取指定页的所有内容到word
 * - 支持提取指定页的表格到excel
 * - 支持pdf小文件的所有内容转成word
 * - 支持pdf小文件的表格转成excel
 * 实现思路：
 * - 若是小的pdf文件(3页以内)，可直接转换成word
 * - 若是大的pdf文件，可以选中某些页(最大支持100页)
 * - 先利用pdfbox将源pdf文件拆分多个单页的
 * - 再将选中的页号对应的pdf转成word
 * - 最后将word合并，完成pdf转word
 * 技术组件
 * - 开源的pdf解析工具：pdfbox、x-easy-pdf(二次封装了pdfbox)，完成pdf拆分
 * -
 * 特别注意：由于采用的工具技术较多，以保证质量为前提，故吞吐量无法保证（经过测试，5MB的pdf，95页，加载需3s，每转换1页需要花1s）
 *
 * @author: zhanglu
 * @date: 2023/7/31 18:56
 * @modify: 修改人信息, 修改内容 2023/7/31 18:56
 */
@Slf4j
public class PdfExtractUtil {

    /**
     * 最大页数
     */
    public static final Integer PAGE_MAX_COUNT = 100;

    public static void main(String[] args) {
        byte[] bytes = pdfToWordByPageNo(FileUtil.readBytes("/Users/zhanglu/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/c9a1c762ab6161f8ffce2831a28327a2/Message/MessageTemp/9600bc6c7aa514051e9c3bfe2546162d/File/分包结算协议--王灵剑重庆＋九回弱电(1).pdf"),
                1, 2, false);
        FileUtil.writeBytes(bytes, "/Users/zhanglu/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/c9a1c762ab6161f8ffce2831a28327a2/Message/MessageTemp/9600bc6c7aa514051e9c3bfe2546162d/File/分包结算协议--王灵剑重庆＋九回弱电(1)2.docx");
    }

    /**
     * pdf小文件转为word（最大3页）
     *
     * @param pdfBytes pdf格式字节数组
     * @return docx格式字节数组
     */
    public static byte[] smallPdfToWord(byte[] pdfBytes) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        try {
            //加载pdf
            XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes));
            Assert.isTrue(document.getPageList().size() <= 3, "无效的参数，页数超出限制，最大3页");
            //加载单页PDF文档
            PdfDocument pdf = new PdfDocument(pdfBytes);
            //设置字体包路径
            PdfDocument.setCustomFontsFolders("/Users/zhanglu/Downloads/serif");
            //单页pdf转为word
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            pdf.saveToStream(out, FileFormat.DOCX);
            out.close();
            pdf.close();
            return out.toByteArray();
        } catch (Exception e) {
            log.error("指定pdf页转word异常", e);
        }
        return new byte[0];
    }

    /**
     * 指定pdf页转word
     * - 将大的pdf文件拆分成单页的pdf文件
     * - 根据输入的页号将选中的pdf转成docx
     * - 将docx合并为一个完整的docx
     *
     * @param pdfBytes pdf格式字节数组
     * @param pageNos  页号，可连续，可跳跃，若输入相同的页号，则重复提取，最大20个元素
     * @return docx格式字节数组
     */
    public static byte[] pdfToWordByPageNo(byte[] pdfBytes, List<Integer> pageNos) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        String tmpDir = FileUtil.getTmpDirPath() + File.separator + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN);
        try {
            //检查输入
            Assert.isTrue(ObjectUtil.isNotEmpty(pageNos), "请输入需要处理的页号");
            //检查指定页的范围，不能超过最大限制
            Assert.isTrue(pageNos.size() <= PAGE_MAX_COUNT, "无效的参数，页数超出限制，最大{}页", PAGE_MAX_COUNT);
            //加载pdf
            XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes));
            //检查指定页的范围，不能超过源文件范围
            Assert.isTrue(pageNos.stream().allMatch(o -> o >= 1), "无效的参数，页号必须大于等于1");
            Assert.isTrue(pageNos.stream().allMatch(o -> o <= document.getPageList().size()), "无效的参数，页号超出文档范围");
            //申请临时文件目录
            FileUtil.mkdir(tmpDir);
            //pdf拆分成单页
            document.splitter().split(tmpDir, "");
            //关闭x-esay-pdf流，后续采用spire技术处理
            document.close();
            //指定页面转成word
            List<byte[]> bytes = new ArrayList<>();
            for (int i = 0; i < pageNos.size(); i++) {
                log.info("pdf to word : {}", tmpDir + File.separator + pageNos.get(i) + ".pdf");
                //加载单页PDF文档
                PdfDocument pdf = new PdfDocument(tmpDir + File.separator + pageNos.get(i) + ".pdf");
                //设置字体包路径
                PdfDocument.setCustomFontsFolders("/Users/zhanglu/Downloads/serif");
                //单页pdf转为word
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                pdf.saveToStream(out, FileFormat.DOCX);
                bytes.add(out.toByteArray());
                out.close();
                pdf.close();
            }
            //word合并
            return mergeWords(bytes);
        } catch (Exception e) {
            log.error("指定pdf页转word异常", e);
        } finally {
            //删除临时目录
            FileUtil.del(tmpDir);
        }
        return new byte[0];
    }

    /**
     * 指定pdf起止页转word
     * - 将大的pdf文件拆分成单页的pdf文件
     * - 根据输入的页号将选中的pdf转成docx
     * - 将docx合并为一个完整的docx
     *
     * @param pdfBytes    pdf格式字节数组
     * @param startPageNo 起始页号，若输入相同的页号，提取输入页
     * @param endPageNo   起始页号，若输入相同的页号，提取输入页，间隔范围最大20个元素
     * @return docx格式字节数组
     */
    public static byte[] pdfToWordByPageNo(byte[] pdfBytes, Integer startPageNo, Integer endPageNo) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        String tmpDir = FileUtil.getTmpDirPath() + File.separator + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN);
        try {
            //检查输入参数
            Assert.isTrue(ObjectUtil.isNotEmpty(startPageNo), "请输入起始页号");
            Assert.isTrue(ObjectUtil.isNotEmpty(startPageNo), "请输入结束页号");
            //检查指定页的范围，不能超过最大限制
            Assert.isTrue((endPageNo - startPageNo) <= PAGE_MAX_COUNT, "无效的参数，页数超出限制，最大{}页", PAGE_MAX_COUNT);
            //加载pdf
            XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes));
            Assert.isTrue(startPageNo >= 1, "无效的参数，起始页号必须大于等于1");
            Assert.isTrue(endPageNo <= document.getPageList().size(), "无效的参数，结束页号超出文档范围");
            //申请临时文件目录
            FileUtil.mkdir(tmpDir);
            //pdf拆分成单页
            document.splitter().split(tmpDir, "");
            //关闭x-esay-pdf流，后续采用spire技术处理
            document.close();
            List<byte[]> bytes = new ArrayList<>();
            for (int i = (startPageNo - 1); i < endPageNo; i++) {
                log.info("pdf to word : {}", tmpDir + File.separator + (i + 1) + ".pdf");
                //加载单页PDF文档
                PdfDocument pdf = new PdfDocument(tmpDir + File.separator + (i + 1) + ".pdf");
                //设置字体包路径
                PdfDocument.setCustomFontsFolders("/Users/zhanglu/Downloads/serif");
                //单页pdf转为word
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                pdf.saveToStream(out, FileFormat.DOCX);
                bytes.add(out.toByteArray());
                out.close();
                pdf.close();
            }
            //word合并
            return mergeWords(bytes);
        } catch (Exception e) {
            log.error("指定pdf起止页转word异常", e);
        } finally {
            //删除临时目录
            FileUtil.del(tmpDir);
        }
        return new byte[0];
    }

    /**
     * 合并word
     *
     * @param bytes docx格式字节数组列表
     * @return 合并后的docx格式字节数组
     */
    public static byte[] mergeWords(List<byte[]> bytes) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            if (ObjectUtil.isEmpty(bytes)) {
                return new byte[0];
            }
            Assert.isTrue(bytes.size() <= PAGE_MAX_COUNT, "无效的参数，页数超出限制，最大{}页", PAGE_MAX_COUNT);
            //创建 Document 类的对象并从磁盘加载 Word 文档
            Document document = new Document();
            for (int i = 0; i < bytes.size(); i++) {
                //子文件合并
                document.insertTextFromStream(new ByteArrayInputStream(bytes.get(i)), com.spire.doc.FileFormat.Docx);
            }
            //输出合并后的word文件
            document.saveToStream(out, com.spire.doc.FileFormat.Docx);
            return out.toByteArray();
        } catch (Exception e) {
            log.error("合并word异常", e);
        }
        return new byte[0];
    }

    /**
     * 获取pdf表格数据
     * - 文件任意大小
     * - 只获取数据
     *
     * @param pdfBytes pdf格式字节数据
     * @param pageNo   指定页
     * @return 表格数据
     */
    public static TableExtractDO getTable(byte[] pdfBytes, Integer... pageNo) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        String tmpDir = FileUtil.getTmpDirPath() + File.separator + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN);
        TableExtractDO tableExtractDO = new TableExtractDO();
        try {
            //加载pdf
            XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes));
            //申请临时文件目录
            FileUtil.mkdir(tmpDir);
            //pdf拆分成单页
            document.splitter().split(tmpDir, "");
            //关闭x-esay-pdf流，后续采用spire技术处理
            document.close();
            for (Integer no : pageNo) {
                //加载单页PDF文档
                PdfDocument pdf = new PdfDocument(tmpDir + File.separator + no + ".pdf");
                //设置字体包路径
                PdfDocument.setCustomFontsFolders("/Users/zhanglu/Downloads/serif");
                //创建实例
                PdfTableExtractor extractor = new PdfTableExtractor(pdf);
                //提取表格
                PdfTable[] pdfTables = extractor.extractTable(0);
                if (ObjectUtil.isEmpty(pdfTables)) {
                    continue;
                }
                for (int j = 0; j < pdfTables.length; j++) {
                    //表格
                    PdfTable table = pdfTables[j];
                    //行数
                    int rowCount = table.getRowCount();
                    //列数
                    int columnCount = table.getColumnCount();
                    //遍历行
                    for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) {
                        //添加行
                        TableExtractDO.Row row = new TableExtractDO.Row(rowIdx);
                        tableExtractDO.addRow(row);
                        //遍历列
                        for (int columnIdx = 0; columnIdx < columnCount; columnIdx++) {
                            //获取表格中的文本内容
                            String text = table.getText(rowIdx, columnIdx);
                            //添加单元格
                            tableExtractDO.addColumn(row, columnIdx, text);
                            //计算列宽
                            tableExtractDO.setColumnSizes(columnIdx, text.length());
                        }
                    }
                }
                pdf.close();
            }
        } catch (Exception e) {
            log.error("获取pdf表格数据异常", e);
        } finally {
            //删除临时目录
            FileUtil.del(tmpDir);
        }
        return tableExtractDO;
    }

    /**
     * 根据表格数据生成excel
     * - 默认样式
     * - 不支持合并
     *
     * @param table 表格数据
     * @return excel格式字节数组
     */
    public static byte[] generateExcel(TableExtractDO table) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            ExcelWriter excelWriter = ExcelUtil.getBigWriter();
            List<List<String>> rows = table.getRowsColumns();
            //设置单元格宽度
            table.getColumnSizeMap().forEach((k, v) -> excelWriter.setColumnWidth(k, (v * 2) > 255 ? 255 : v * 2));
            //垂直居中
            excelWriter.getCellStyle().setVerticalAlignment(VerticalAlignment.CENTER);
            //水平居左
            excelWriter.getCellStyle().setAlignment(HorizontalAlignment.LEFT);
            //自动单元格大小
            excelWriter.autoSizeColumnAll();
            // 一次性写出内容，使用默认样式
            excelWriter.write(rows);
            //输出到字节流
            excelWriter.getWorkbook().write(out);
            //关闭writer，释放内存
            excelWriter.close();
            return out.toByteArray();
        } catch (Exception e) {
            log.error("生成excel异常", e);
            return new byte[0];
        }
    }

    /**
     * 提取任意pdf指定单页的表格
     * - 支持大文件
     * - 仅支持提取单页的表格
     *
     * @param pdfBytes pdf格式字节数据
     * @return excel格式字节数组
     */
    public static byte[] extractExcel(byte[] pdfBytes, Integer pageNo) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        String tmpDir = FileUtil.getTmpDirPath() + File.separator + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN);
        try {
            //加载pdf
            XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes));
            //申请临时文件目录
            FileUtil.mkdir(tmpDir);
            //pdf拆分成单页
            document.splitter().split(tmpDir, "");
            //关闭x-esay-pdf流，后续采用spire技术处理
            document.close();
            //临时pdf文件
            String tmpPdf = tmpDir + File.separator + pageNo + ".pdf";
            //pdf小文件转为excel表格
            return smallPdfToExcel(FileUtil.readBytes(tmpPdf));
        } catch (Exception e) {
            log.error("获取pdf表格数据异常", e);
        } finally {
            //删除临时目录
            FileUtil.del(tmpDir);
        }
        return new byte[0];
    }

    /**
     * pdf小文件转为excel表格（3页以内，包含三页面）
     *
     * @param pdfBytes pdf格式字节数据
     * @return excel格式字节数组
     */
    public static byte[] smallPdfToExcel(byte[] pdfBytes) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        try {
            //加载pdf
            XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes));
            Assert.isTrue(document.getPageList().size() <= 3, "无效的参数，页数超出限制，最大3页");
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            PdfDocument pdf = new PdfDocument();
            //加载pdf
            pdf.loadFromBytes(pdfBytes);
            //pdf转xlsx
            pdf.saveToStream(out, FileFormat.XLSX);
            pdf.close();
            return out.toByteArray();
        } catch (Exception e) {
            log.error("pdf小文件转为excel表格异常", e);
        }
        return new byte[0];
    }

//    /**
//     * 文本提取
//     * @param pdfBytes  pdf格式字节数据
//     * @param pageNos   页号，空则，获取全部内容
//     * @return          文本信息
//     */
//    public static List<XEasyPdfDocumentAnalyzer.TextInfo> textExtract(byte[] pdfBytes, int... pageNos){
//        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
//        //加载pdf
//        try (XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes))){
//            XEasyPdfDocumentAnalyzer analyzer = document.analyzer();
//            analyzer.analyzeText(pageNos);
//            //提取文本
//            return analyzer.getTextInfoList();
//        } catch (Exception e){
//            log.error("pdf文本提取异常", e);
//        }
//        return null;
//    }
//
//    /**
//     * 文本提取
//     * @param pdfBytes      pdf格式字节数据
//     * @param startPageNo   起始页号
//     * @param endPageNo     结束页号
//     * @return              文本信息
//     */
//    public static List<XEasyPdfDocumentAnalyzer.TextInfo> textExtract(byte[] pdfBytes, Integer startPageNo, Integer endPageNo){
//        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
//        Assert.isTrue(ObjectUtil.isAllNotEmpty(startPageNo, endPageNo), "无效的参数，请输入起止页号");
//        Assert.isFalse(endPageNo <= startPageNo , "无效的参数，结束页号必须小于等于起始页号");
//        //加载pdf
//        try (XEasyPdfDocument document = XEasyPdfHandler.Document.load(new ByteArrayInputStream(pdfBytes))){
//            XEasyPdfDocumentAnalyzer analyzer = document.analyzer();
//            List<Integer> pageNoList = new ArrayList<>();
//            for (Integer i = startPageNo; i <= endPageNo; i++) {
//                pageNoList.add(i);
//            }
//            int[] pageNos = new int[pageNoList.size()];
//            analyzer.analyzeText(pageNos);
//            //提取文本
//            return analyzer.getTextInfoList();
//        } catch (Exception e){
//            log.error("pdf文本提取异常", e);
//        }
//        return null;
//    }

    /**
     * 获取文档信息
     * @param pdfBytes  pdf格式字节数据
     * @return          文档信息（基础信息、元数据信息、加密信息）
     */
    public static DocumentInfoDO getDocumentInfo(byte[] pdfBytes){
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        DocumentInfoDO info = new DocumentInfoDO();
        try (PDDocument document = PDDocument.load(pdfBytes)) {
            PDDocumentInformation information = document.getDocumentInformation();
            info.setPageNumTotal(document.getNumberOfPages());
            info.setVersion(document.getVersion() + "");
            info.setAuthor(information.getAuthor());
            info.setCreator(information.getCreator());
            info.setCreateDate(DateUtil.format(information.getCreationDate().getTime(), DatePattern.NORM_DATETIME_MS_PATTERN));
            info.setUpdateDate(DateUtil.format(information.getModificationDate().getTime(), DatePattern.NORM_DATETIME_MS_PATTERN));
            info.setTitle(information.getTitle());
            info.setSubject(information.getSubject());
            info.setKeywords(information.getKeywords());
            info.setProducer(information.getProducer());
            Set<String> metadataKeys = information.getMetadataKeys();
            for (String metadataKey : metadataKeys) {
                String customMetadataValue = information.getCustomMetadataValue(metadataKey);
                Object propertyStringValue = information.getPropertyStringValue(metadataKey);
                info.getMetadata().put(metadataKey, customMetadataValue);
                info.getProperties().put(metadataKey, propertyStringValue);
            }
            PDEncryption encryption = document.getEncryption();
            if(ObjectUtil.isNotEmpty(encryption)){
                info.setEncryption(new DocumentInfoDO.Encryption());
                int version = encryption.getVersion();
                int length = encryption.getLength();
                byte[] ownerEncryptionKey = encryption.getOwnerEncryptionKey();
                byte[] ownerKey = encryption.getOwnerKey();
                int permissions = encryption.getPermissions();
                byte[] perms = encryption.getPerms();
                int revision = encryption.getRevision();
                byte[] userEncryptionKey = encryption.getUserEncryptionKey();
                byte[] userKey = encryption.getUserKey();
                int recipientsLength = encryption.getRecipientsLength();
                String filter = encryption.getFilter();
                String subFilter = encryption.getSubFilter();
                info.getEncryption().setVersion(version);
                info.getEncryption().setLength(length);
                info.getEncryption().setVersion(version);
                info.getEncryption().setOwnerEncryptionKey(ownerEncryptionKey);
                info.getEncryption().setOwnerKey(ownerKey);
                info.getEncryption().setPermissions(permissions);
                info.getEncryption().setPerms(perms);
                info.getEncryption().setRevision(revision);
                info.getEncryption().setUserEncryptionKey(userEncryptionKey);
                info.getEncryption().setUserKey(userKey);
                info.getEncryption().setRecipientsLength(recipientsLength);
                info.getEncryption().setFilter(filter);
                info.getEncryption().setSubFilter(subFilter);
            }
        } catch (Exception e) {
            log.error("根据页号文本行提取异常", e);
        }
        return info;
    }

    /**
     * 根据页号文本行提取
     *
     * @param pdfBytes pdf格式字节数据
     * @return 文本信息
     *  - 注意：在PDF中，有些没有意义的数据，例如页号会占一行
     */
    public static List<TextLineExtractDO> textLineExtractByPageNo(byte[] pdfBytes, List<Integer> pageNos) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        List<TextLineExtractDO> textLines = new ArrayList<>();
        //加载pdf
        try (PDDocument document = PDDocument.load(pdfBytes)) {
            PDFTextStripper pdfStripper = new PDFTextStripper();
            //PDF文件中文本标记的顺序可能与它们在屏幕上显示的顺序不同。
            //例如，PDF编写器可能会按字体写出所有文本，因此所有文本都是粗体或更大的文本，然后进行第二次遍历并写出正常文本。默认情况是不按位置排序。
            //PDF编写器可以选择以不同的顺序编写每个字符。默认情况下，由于性能原因，PDFBox在处理文本标记之前不会对其进行排序。
            pdfStripper.setSortByPosition(true);
            //默认情况下，文本剥离器将尝试删除相互重叠的文本。Word多次绘制同一个字符以使其看起来大胆。通过将其设置为false，将提取所有文本，这意味着某些部分将被复制，但会注意到更好的性能。
            pdfStripper.setSuppressDuplicateOverlappingText(false);
            //行分隔符
            pdfStripper.setLineSeparator("------EOF");
            pdfStripper.setPageEnd("------EOF");
            //空则获取全部页的文本行
            if(ObjectUtil.isEmpty(pageNos)){
                pageNos = new ArrayList<>();
                for (int i = 1; i <= document.getNumberOfPages(); i++) {
                    pageNos.add(i);
                }
            }
            for (int pageNo : pageNos) {
                pdfStripper.setStartPage(pageNo);
                pdfStripper.setEndPage(pageNo);
                String text = pdfStripper.getText(document);
                List<String> lines = Arrays.stream(text.split("------EOF"))
                        .filter(o -> ObjectUtil.isNotEmpty(o) && ObjectUtil.isNotEmpty(o.trim()))
                        .map(o -> o.trim())
                        .collect(Collectors.toList());
                textLines.add(new TextLineExtractDO().setPageNo(pageNo).setLines(lines));
            }
            return textLines;
        } catch (Exception e) {
            log.error("根据页号文本行提取异常", e);
        }
        return textLines;
    }

    /**
     * 根据起止页号文本行提取
     *
     * @param pdfBytes    pdf格式字节数据
     * @param startPageNo 起始页号
     * @param endPageNo   结束页号
     * @return 文本信息
     *  - 注意：在PDF中，有些没有意义的数据，例如页号会占一行
     */
    public static List<String> textLineExtractByPageNo(byte[] pdfBytes, Integer startPageNo, Integer endPageNo) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), "请输入有效的pdf文件");
        Assert.isTrue(ObjectUtil.isAllNotEmpty(startPageNo, endPageNo), "无效的参数，请输入起止页号");
        Assert.isTrue(endPageNo > startPageNo, "无效的参数，结束页号必须大于起始页号");
        Assert.isTrue(startPageNo >= 1, "无效的参数，起止页号必须大于等于1");
        //加载pdf
        try (PDDocument document = PDDocument.load(pdfBytes)) {
            PDFTextStripper pdfStripper = new PDFTextStripper();
            pdfStripper.setStartPage(startPageNo);
            pdfStripper.setEndPage(endPageNo);
            //PDF文件中文本标记的顺序可能与它们在屏幕上显示的顺序不同。
            //例如，PDF编写器可能会按字体写出所有文本，因此所有文本都是粗体或更大的文本，然后进行第二次遍历并写出正常文本。默认情况是不按位置排序。
            //PDF编写器可以选择以不同的顺序编写每个字符。默认情况下，由于性能原因，PDFBox在处理文本标记之前不会对其进行排序。
            pdfStripper.setSortByPosition(true);
            //默认情况下，文本剥离器将尝试删除相互重叠的文本。Word多次绘制同一个字符以使其看起来大胆。通过将其设置为false，将提取所有文本，这意味着某些部分将被复制，但会注意到更好的性能。
            pdfStripper.setSuppressDuplicateOverlappingText(false);
            //行分隔符
            pdfStripper.setLineSeparator("------EOF");
            pdfStripper.setPageEnd("------EOF");
            String text = pdfStripper.getText(document);
            List<String> texts = Arrays.stream(text.split("------EOF"))
                    .filter(o -> ObjectUtil.isNotEmpty(o) && ObjectUtil.isNotEmpty(o.trim()))
                    .map(o -> o.trim())
                    .collect(Collectors.toList());
            return texts;
        } catch (Exception e) {
            log.error("根据起止页号文本行提取异常", e);
        }
        return new ArrayList<>();
    }

    /**
     * 指定pdf起止页转word - 统一设置输出文档模式
     * - 将大的pdf文件拆分成单页的pdf文件
     * - 根据输入的页号将选中的pdf转成docx
     * - 将docx合并为一个完整的docx
     *
     * @param pdfBytes    pdf格式字节数组
     * @param startPageNo 起始页号，若输入相同的页号，提取输入页
     * @param endPageNo   起始页号，若输入相同的页号，提取输入页，间隔范围最大{@link CustomConstant#PDF_TO_WORD_PAGE_MAX_COUNT}个元素
     * @param convertToWordUsingFlow  是否输出为流式文档结构，流式易编辑但对于表格效果较差，版式不易编辑对于表格效果较好
     * @return docx格式字节数组
     */
    public static byte[] pdfToWordByPageNo(byte[] pdfBytes, Integer startPageNo, Integer endPageNo, boolean convertToWordUsingFlow) {
        Assert.isTrue(ObjectUtil.isNotEmpty(pdfBytes), () -> new RuntimeException(CustomConstant.PDF_FILE_INVALID));
        //检查输入参数
        Assert.isTrue(ObjectUtil.isNotEmpty(startPageNo), () -> new RuntimeException("请输入起始页号"));
        Assert.isTrue(ObjectUtil.isNotEmpty(endPageNo), () -> new RuntimeException("请输入结束页号"));
        //检查指定页的范围，不能超过最大限制
        Assert.isTrue((endPageNo - (startPageNo - 1)) <= CustomConstant.PDF_TO_WORD_PAGE_MAX_COUNT, () -> new RuntimeException(CharSequenceUtil.format(CustomConstant.PDF_TO_WORD_PAGE_LIMIT_TIP, CustomConstant.PDF_TO_WORD_PAGE_MAX_COUNT)));
        String tmpDir = FileUtil.getTmpDirPath() + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN) + StrPool.UNDERLINE + RandomUtil.randomNumbers(4);
        try (ByteArrayInputStream out = new ByteArrayInputStream(pdfBytes)) {
            //加载pdf
            XEasyPdfDocument document = XEasyPdfHandler.Document.load(out);
            Assert.isTrue(startPageNo >= 1, () -> new RuntimeException("无效的参数，起始页号必须大于等于1"));
            Assert.isTrue(endPageNo <= document.getPageList().size(), () -> new RuntimeException("无效的参数，结束页号超出文档范围"));
            //申请临时文件目录
            FileUtil.mkdir(tmpDir);
            //pdf拆分成单页
            document.splitter().split(tmpDir, "");
            //关闭x-esay-pdf流，后续采用spire技术处理
            document.close();
            List<byte[]> bytes = new ArrayList<>();
            for (int i = (startPageNo - 1); i < endPageNo; i++) {
                log.info("pdf to word by range: {}", (i + 1) + ".pdf");
                //加载单页PDF文档
                PdfDocument pdf = new PdfDocument(tmpDir + File.separator + (i + 1) + ".pdf");
                //转换可选项
                PdfConvertOptions convertOptions = pdf.getConvertOptions();
                //是否输出为流式文档结构，流式易编辑但对于表格效果较差，版式不易编辑对于表格效果较好
                convertOptions.setConvertToWordUsingFlow(convertToWordUsingFlow);
                //单页pdf转为word
                try (ByteArrayOutputStream ou = new ByteArrayOutputStream()) {
                    bytes.add(SpireServiceLimit.pdfConvertAndWordMerge(() -> new byte[0], () -> {
                        pdf.saveToStream(ou, FileFormat.DOCX);
                        pdf.close();
                        return ou.toByteArray();
                    }));
                }
            }
            //word合并
            return WordMergeUtil.mergeWords(bytes);
        } catch (Exception e) {
            log.error("指定pdf起止页转word异常", e);
        } finally {
            //删除临时目录
            FileUtil.del(tmpDir);
        }
        return new byte[0];
    }

//    private static void textExtract(PDDocument document, PDFTextStripper pdfStripper, StringBuilder builder, List<Integer> pageNoList) throws Exception {
//        if (ObjectUtil.isEmpty(pageNoList)) {
//            builder.append(pdfStripper.getText(document));
//            return;
//        }
//        if (isOrder(pageNoList)) {
//            int[] pageNos = new int[pageNoList.size()];
//            pdfStripper.setStartPage(pageNos[0]);
//            pdfStripper.setEndPage(pageNos[pageNos.length - 1] + 1);
//            builder.append(pdfStripper.getText(document));
//            return;
//        }
//        for (int pageNo : pageNoList) {
//            pdfStripper.setStartPage(pageNo);
//            pdfStripper.setEndPage(pageNo + 1);
//            builder.append(pdfStripper.getText(document));
//        }
//    }
//
//    private static boolean isOrder(List<Integer> arr) {
//        if (ObjectUtil.isEmpty(arr)) {
//            return false;
//        }
//        boolean ret = false;
//        for (int i = 0; i < arr.size() - 1; i++) {
//            if (arr.get(i) > arr.get(i + 1)) {
//                ret = true;
//            }
//        }
//        return ret;
//    }

}
